API tutorial: development of a sample application
This section offers a tutorial for fcrypt, a sample application that is provided with ProtectToolkit-C.
The fcrypt application enables files to be encrypted for a given recipient and then decrypted by that recipient. Since the encrypted file contains a message authentication code (MAC), the recipient of a document will also be able to verify that the encrypted file was not modified.
In order to follow this example effectively, the reader is strongly encouraged to open or print the source of the application as a reference. The source code for fcrypt can be found in the file fcrypt.c within your chosen install directory.
Note
To avoid running into issues, move samples out of the installation directory before modifying, compiling, or running them.
Required header files
You will note in the initial code segments that, apart from the standard header files, we include the ProtectToolkit-C set of required library files.
Whereas cryptoki.h is the required PKCS#11 header, the remainder implement some of the advanced or extended features of the ProtectToolkit-C implementation, such as error feedback.
Runtime switches
We want to develop fcrypt to be able to take a series of command line inputs to allow us to decrypt a message, use password-based encryption (pbe) or to display time information for a cipher operation. With that in mind, the following flags are defined appropriately.
Encrypt functions
-
For our file encryption and subsequent decryption, we define the following two functions:
We want the encrypt function to take the public key of the receiving party (recipient), encrypt the data (ifile) with the given key and sign the encrypted data with the sender's private key (sender), before outputting and encoding the file to the output file (ofile).
For error handling purposes, we define the function as follows:
-
We now need to define the required PKCS#11 data types pertaining to the session, slot identification, and object handles we will use for the sender and recipient keys.
-
We must also allocate variables to define the type of mechanism, digest, and key information during encryption.
Earlier, we said that we wanted to be able to perform password-based encryption via a runtime switch, so accordingly this is the first instance that we check for with our pflag variable.
-
Our next step is to define our secret key that we will use to encrypt the data. The key type to be used is double-length DES. The CK_BBOOL refers to a byte-sized Boolean flag that we have defined as either
TRUE
orFALSE
for easier reference.CK_ATTRIBUTE is a structure that includes the type, value, and length of an attribute. Since every PKCS#11 key object is required to be assigned certain attributes, this structure is later used during our key derivation and generation to assign those attributes to the key.
-
The params variable is defined using the PKCS#11 definition CK_PBE_PARAMS, a structure that provides all of the necessary information required by the PKCS#11 password-based encryption mechanisms.
-
PKCS#11 also uses a structure for defining the mechanism. Within
CK_MECHANISM
we need to specify the mechanism type, a pointer to the parameters we defined earlier and the size of the parameters. The mechanism type we will use isCKM_PBE_SHA1_DES2_EDE_CBC
that is used for generating a 2-key triple-DES secret key and IV from a password and a salt value by using the SHA-1 digest algorithm and an iteration count. -
We have now set up our required structures, and the next step is to open a session between the application and a token in a particular slot using the PKCS#11 call C_OpenSession. This call requires the slot ID flags which indicate the type of session, an application-defined pointer to be passed to the notification callback; an address of the notification callback function, and a pointer to the location that receives the handle for the new session.
-
Once we have successfully opened a session with the token, we now want to generate the key that we will use to encrypt our input file. The C_GenerateKey function will generate a secret key and thereby create a new key object. This function call requires the session’s handle, a pointer to the key generation mechanism, a pointer to the template for the new key, the number of attributes in the template and a pointer to the location that receives the handle of the new key.
The CHECK_RV() function call is part of the ProtectToolkit-C extended capability for better error feedback and handling.
-
If we are not using the password-based encryption switch at program execution, the desired reaction is to perform file encryption using RSA, and hence we will need to generate the secret key value for the operation.
The function FindKeyFromName is part of the ProtectToolkit-C CTUTIL library to provide extended functionality. It is used here to locate the keys which are passed into fcrypt at the command line and return the slot ID, session handle and object handle of those keys.
-
To achieve acceptable performance during file encryption and decryption we need to use a symmetric key cipher such as DES. The DES key we generate for this purpose is to be wrapped with the recipient’s RSA key so it can later be unwrapped and used for decryption without the value of the key ever being known.
-
Rather than simply using the same key for each file encryption, we will generate a random DES key for each encryption of the input file. The mechanism used here is
CKM_DES2_KEY_GEN
that is used for generating double-length DES keys. -
The key wrapping is performed with the C_WrapKey function that encrypts (wraps) a private or secret key. The function requires the session handle, the wrapping mechanism, the handle of the wrapping key, the handle of the key to be wrapped, a pointer to the location that receives the wrapped key and a pointer to the location that receives the length of the wrapped key.
-
For the wrapping mechanism we will choose
CKM_RSA_PKCS
that is a multi-purpose mechanism based on the RSA public-key cryptosystem and the block formats defined in PKCS #1. It supports single-part encryption and decryption, single-part signatures and verification with and without message recovery, key wrapping and key unwrapping.
-
-
Now that we have a random secret key to perform the encryption with, we will need to set the required mechanism and parameters prior to encrypting the input file. As a mechanism for the encryption we will choose CKM_DES3_CBC_PAD which is using triple-DES in Cipher Block Chaining mode and PKCS#1 padding.
An application cannot call C_Encrypt in a session without having called C_EncryptInit first to activate an encryption operation. C_EncryptInit requires the session’s handle, a pointer to the encryption mechanism and the handle of the encryption key.
In the same manner as we initialized and set up, our digest operation is to be the signature verification to send along to the recipient with the encrypted data. The mechanism used for our digest is SHA-1 that is defined in PKCS#11 terms as
CKM_SHA_1
. -
We are now ready to process our input file by encrypting the data, generating the message digest and writing the output to file.
If the password based encryption switch wasn’t set, the first instance we write to file is the DES secret key wrapped by the recipient’s public key.
-
Since our mode of encryption is cipher block chaining (CBC) we need to perform our output using four definitive looping steps until our data is processed.
-
For the digest we use the PKCS#11 function C_Digest_Update, which continues a multiple-part message-digesting operation, processing another data part. The function requires the session handle, a pointer to the data part and the length of the data part.
-
For the encryption, we use C_EncryptUpdate, which continues a multiple-part encryption operation, processing another data part. The function requires the session handle, a pointer to the data part; the length of the data part; a pointer to the location that receives the encrypted data part and a pointer to the location that holds the length in bytes of the encrypted data part.
-
-
Once all the data has been processed, we need to finalize the encryption and digest operation.
-
To finish the encryption, we use the C_EncryptFinal call, which finishes a multiple-part encryption operation. The function requires the session handle, a pointer to the location that receives the last encrypted data part, if any, and a pointer to the location that holds the length of the last encrypted data part.
-
For finalizing the digest, we call C_DigestFinal, which finishes a multiple-part message-digesting operation, returning the message digest. The function requires the session’s handle, a pointer to the location that receives the message digest and a pointer to the location that holds the length of the message digest.
-
-
If the password-based encryption flag was set, we use the digest created in the above process as our signature, since there is no recipient key to sign the data with. For our DES encryption we will sign the digest with our recipient’s public key.
-
The function C_SignInit is our first call and initializes a signature operation, where the signature is an appendix to the data. The function requires the session’s handle, a pointer to the signature mechanism and the handle of the signature key.
-
We also need to specify a mechanism to use for our signature operation, in this case
CKM_RSA_PKCS
, which is an RSA PKCS #1 mechanism. -
The signature generation is performed with the call to C_Sign that signs data in a single part, where the signature is an appendix to the data. The function requires the session’s handle, a pointer to the data, the length of the data, a pointer to the location that receives the signature, and a pointer to the location that holds the length of the signature.
-
Decrypt function
For our decryption, we want to basically reverse the processes that were covered previously in the encryption section.
-
Following the initial function setup, we firstly check for our input and output files.
-
Once file existence is established, we test for our password-based encryption runtime switch. It can be seen that once again we generate the same secret key from the input password that we will need for the decryption. Since this was a secret key cipher, we use the same key for encryption as well as decryption.
-
For our public key cipher, we will use the recipient’s private RSA key to unwrap the secret DES key contained in the input file. The DES key will then be used to decrypt the file.
The PKCS#11 function C_UnwrapKey is used to decrypt (unwrap) a wrapped key, creating a new private key or secret key object. This function requires the session handle, a pointer to the unwrapping mechanism, the handle of the unwrapping key, a pointer to the wrapped key, the length of the wrapped key, a pointer to the template for the new key, the number of attributes in the template, and a pointer to the location that receives the handle of the recovered key.
-
Now that we have recovered the decryption key, we perform our initialization in exactly the same manner as for our encryption, but using the function C_DecryptInit. The digest is calculated in the same manner used for the encryption.
-
For the file decryption we are using the functions C_DecryptUpdate and C_DecryptFinal which take the same parameters as their encrypt counterparts.
-
Finally, we verify the signature contained in the data file. Since the signature is identical to the digest when using the password-based encryption option, it is a simple matter of comparing the two. For our DES encryption on the other hand, we need to verify the signature against the sender’s public key.
To perform this we start by calling C_VerifyInit that initializes a verification operation, where the signature is an appendix to the data. This function requires the session’s handle, a pointer to the structure that specifies the verification mechanism and the handle of the verification key.
fcrypt usage
When no command line inputs are received by the application, it can be useful to show the required inputs on screen in a help context.
Wrapped encryption key template
The DES encryption key that we wrap with the user RSA key will need to have its attributes specified within a template as follows:
Assembling the application
-
Now bring all the required components for the fcrypt application together in the main application body.
-
The first call within a PKCS#11 application must be C_Initialize, which initializes the PKCS#11 library. The function takes as an argument either value NULL_PTR or points to a CK_C_INITIALIZE_ARGS structure containing information on how the library should deal with multi-threaded access - no threading information is required for ProtectToolkit-C, so a NULL_PTR is used as the argument.
-
The function call to CT_ErrorString is part of the ProtectToolkit-C extended capability within ctutil.h and converts a PKCS#11 error code into a printable string.
-
Since Thales supports versions of PKCS#11 that are incompatible with one another, the CheckCryptokiVersion function is called to ensure that an application compiled for V1.X compliance is not going to fail if it links against a V 2.X-compliant DLL and vice versa. This function is part of the extended ProtectToolkit-C functionality within ctutil.h and ensures that the version of PKCS#11 is correct.
-
When the application is done using PKCS#11, it calls the PKCS#11 function C_Finalize and ceases to be a PKCS#11 application. It should be the last PKCS#11 call made by an application. The parameter is reserved for future versions and should be set to NULL_PTR.